/*
 * Copyright 2012 Marsrutai.lt
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lt.marsrutai.mobile.android.service;

import static lt.marsrutai.mobile.android.service.EntityColumns.FIELD_ID;
import static lt.marsrutai.mobile.android.service.EntityColumns.FIELD_TYPE;
import static lt.marsrutai.mobile.android.util.SQLiteWhereQueryBuilder.Operators.EQ;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import lt.marsrutai.mobile.android.dao.Identifier;
import lt.marsrutai.mobile.android.model.Entity;
import lt.marsrutai.mobile.android.util.BeanUtils;
import lt.marsrutai.mobile.android.util.BeanUtils.BeanProperty;
import lt.marsrutai.mobile.android.util.ClassUtils;
import lt.marsrutai.mobile.android.util.SQLiteUtils;
import lt.marsrutai.mobile.android.util.SQLiteWhereQueryBuilder;
import lt.marsrutai.mobile.android.util.StringUtils;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;

/**
 * 
 * @author jvalskis
 *
 * @param <T>
 */
public abstract class AbstractObjectStoreSQLiteHelper<T extends Entity> extends SQLiteOpenHelper {

    public static final int ENTITY_ADDED = 1;
    public static final int ENTITY_UPDATED = 2;
    public static final int ENTITY_NOT_ADDED = -1;
    public static final int ENTITY_NOT_UPDATED = -2;

    public AbstractObjectStoreSQLiteHelper(Context context, String tableName, CursorFactory cursorFactory, int databaseVersion) {
        super(context, tableName, cursorFactory, databaseVersion);
    }

    protected void addColumn(SQLiteDatabase db, String columnName, String columnType) {
        db.execSQL("ALTER TABLE " + getTableName() + " ADD COLUMN " + columnName + " " + columnType + ";");
    }

    /**
     * Builds CREATE TABLE query from column definitions ({@link #getColumnDefinitions()})
     * 
     * @return
     */
    protected String getCreateTableQuery() {
        List<String> fieldQueryParts = new ArrayList<String>();
        for (ColumnDefinition fieldDefinition : getColumnDefinitions()) {
            fieldQueryParts.add(fieldDefinition.toString());
        }

        String fieldsQuery = StringUtils.join(fieldQueryParts, ", ");

        return String.format("CREATE TABLE %s (%s);", getTableName(), fieldsQuery);
    }

    protected abstract String getTableName();

    /**
     * <p>
     * Column definitions.
     * </p>
     * 
     * @see ColumnDefinition
     */
    protected abstract ColumnDefinition[] getColumnDefinitions();

    /**
     * Inserts an entity
     * 
     * @param item
     * @return <code>true</code> is entity was inserted, <code>false</code> otherwise
     */
    public boolean add(T item) {
        // Gets the data repository in write mode
        SQLiteDatabase db = getWritableDatabase();

        ContentValues values = getContentValues(item);

        long rowId = db.insert(getTableName(), null, values);

        if (rowId > -1) {
            item.setId(rowId);
        }

        return rowId > -1;
    }

    /**
     * Deletes an entity
     * 
     * @param item
     * @return <code>true</code> is entity was removed, <code>false</code> otherwise
     */
    public boolean remove(T entity) {
        return delete(entity.getId() == null ? findByExample(entity) : entity);
    }

    private boolean delete(T entity) {
        SQLiteDatabase db = getWritableDatabase();

        SQLiteWhereQueryBuilder builder = SQLiteWhereQueryBuilder.start(FIELD_ID, EQ, entity.getId());
        return db.delete(getTableName(), builder.get(), builder.getArguments()) == 1;
    }

    /**
     * Updates the provided entity. {@link Entity#getId()} must be present.
     * 
     * @param entity
     * @return
     */
    public boolean update(T entity) {
        entity = entity.getId() == null ? findByExample(entity) : entity;
        
        if (entity != null) {
            SQLiteDatabase db = getWritableDatabase();

            SQLiteWhereQueryBuilder builder = SQLiteWhereQueryBuilder.start(FIELD_ID, EQ, entity.getId());
            return db.update(getTableName(), getContentValues(entity), builder.get(), builder.getArguments()) == 1;
        }
        return false;
    }

    public int updateOrAdd(T entity) {
        T existingEntity = entity.getId() == null ? findByExample(entity) : entity;
        
        if (existingEntity == null) {
            return add(entity) ? ENTITY_ADDED : ENTITY_NOT_ADDED;
        }
        return update(existingEntity) ? ENTITY_UPDATED : ENTITY_NOT_UPDATED;
    }

    protected ContentValues getContentValues(T entity) {
        Set<String> columnNames = getColumnNames();

        // Create a new map of values, where column names are the keys
        ContentValues values = new ContentValues();

        values.put(FIELD_TYPE, entity.getClass().getSimpleName());
        for (BeanProperty property : BeanUtils.getProperties(entity.getClass())) {
            if (columnNames.contains(property.getName())) {
                SQLiteUtils.storeProperty(values, entity, property);
            }
        }
        return values;
    }

    public T findByExample(T example) {
        SQLiteDatabase db = getReadableDatabase();

        @SuppressWarnings("unchecked")
        Class<T> klass = (Class<T>) example.getClass();

        List<BeanProperty> propertiesToSelect = getPropertiesToSelect(klass);
        String[] columnNames = new String[propertiesToSelect.size()];
        for (int i = 0; i < propertiesToSelect.size(); i++) {
            columnNames[i] = propertiesToSelect.get(i).getName();
        }

        SQLiteWhereQueryBuilder queryBuilder = SQLiteWhereQueryBuilder.start();
        for (BeanProperty property : getSelectableProperties(propertiesToSelect)) {
            Object value = BeanUtils.getPropertyValue(example, property);
            
            queryBuilder.add(property.getName(), value == null ? SQLiteWhereQueryBuilder.Operators.IS : SQLiteWhereQueryBuilder.Operators.EQ, value);
        }
        queryBuilder.add(FIELD_TYPE, SQLiteWhereQueryBuilder.Operators.EQ, klass.getSimpleName());
        
        String whereQuery = queryBuilder.get();
        String[] arguments = queryBuilder.getArguments();

        Cursor cursor = db.query(getTableName(), columnNames, whereQuery, arguments, null, null, FIELD_ID + " DESC");
        List<T> bookmarks = getResults(cursor, klass, propertiesToSelect);
        
        if (bookmarks.isEmpty()) {
            return null;
        }
        
        return bookmarks.get(0);
    }

    private List<BeanProperty> getSelectableProperties(List<BeanProperty> propertiesToSelect) {
        List<BeanProperty> result = new ArrayList<BeanProperty>();

        for (BeanProperty property : propertiesToSelect) {
            if (property.getGetter().isAnnotationPresent(Identifier.class)) {
                result.add(property);
            }
        }

        return result;
    }

    public boolean exists(T entity) {
        return findByExample(entity) != null;
    }

    public <E extends T> List<E> find(Class<E> entityClass, String sortColumn) {
        SQLiteDatabase db = getReadableDatabase();

        List<BeanProperty> propertiesToSelect = getPropertiesToSelect(entityClass);
        String[] columnNames = new String[propertiesToSelect.size()];
        for (int i = 0; i < propertiesToSelect.size(); i++) {
            columnNames[i] = propertiesToSelect.get(i).getName();
        }
        
        SQLiteWhereQueryBuilder builder = SQLiteWhereQueryBuilder.start(FIELD_TYPE, SQLiteWhereQueryBuilder.Operators.EQ, entityClass.getSimpleName());

        Cursor cursor = db.query(getTableName(), columnNames, builder.get(), builder.getArguments(), null, null,
                sortColumn == null ? null : sortColumn + " COLLATE NOCASE");
        List<E> results = getResults(cursor, entityClass, propertiesToSelect);

        return results;
    }

    protected <E extends T> List<E> getResults(Cursor cursor, Class<E> klass, List<BeanProperty> propertiesToSelect) {
        List<E> result = new ArrayList<E>();

        try {
            while (cursor.moveToNext()) {
                E entity = ClassUtils.instantiateClass(klass);

                // fill object with data
                for (BeanProperty beanProperty : propertiesToSelect) {
                    BeanUtils.setPropertyValue(entity, beanProperty, SQLiteUtils.retrieveProperty(cursor, beanProperty));
                }

                result.add(entity);
            }
        }
        finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return result;
    }

    private <E extends T> List<BeanProperty> getPropertiesToSelect(Class<E> klass) {
        Set<String> columnNames = getColumnNames();

        List<BeanProperty> propertiesToSelect = new ArrayList<BeanProperty>();
        for (BeanProperty property : BeanUtils.getProperties(klass)) {
            if (columnNames.contains(property.getName())) {
                propertiesToSelect.add(property);
            }
        }

        return propertiesToSelect;
    }

    private Set<String> getColumnNames() {
        Set<String> columnNames = new HashSet<String>();

        for (ColumnDefinition fieldDefinition : getColumnDefinitions()) {
            columnNames.add(fieldDefinition.getName());
        }
        return columnNames;
    }
    
    /**
     * Defines column properties.
     * 
     * @author jvalskis
     */
    protected static class ColumnDefinition {
        private String name;
        private String type;
        private String extra;
        
        public ColumnDefinition(String name, String type, String extra) {
            this.name = name;
            this.type = type;
            this.extra = extra;
        }
        
        public static ColumnDefinition of(String name, String type) {
            return new ColumnDefinition(name, type, null);
        }
        
        public static ColumnDefinition of(String name, String type, String extra) {
            return new ColumnDefinition(name, type, extra);
        }

        public String getName() {
            return name;
        }
        
        public String getType() {
            return type;
        }
        
        public String getExtra() {
            return extra;
        }
        
        public String toString() {
            List<String> parts = new ArrayList<String>();
            parts.add(name);
            parts.add(type);
            
            if (extra != null) {
                parts.add(extra);
            }
            return StringUtils.join(parts, " ");
        }
    }
}
